package com.gaofans.vertx.boot.common.verticle;

import io.vertx.core.Context;
import io.vertx.core.Vertx;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.Scope;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 自定义的Verticle Scope
 * @author gaofans
 */
public class VerticleInstanceScope implements Scope, BeanFactoryPostProcessor {

    private final Vertx vertx;

    public static final String VERTICLE_INSTANCE_NAME = "verticle-instance";

    private static final String VERTICLE_INSTANCE_SCOPE = "verticle-instance-scope";

    public VerticleInstanceScope(Vertx vertx) {
        this.vertx = vertx;
    }

    @Override
    public Object get(String name, ObjectFactory<?> objectFactory) {
        return getCurrentScopeContainer().getOrCreate(name, objectFactory);
    }

    @Override
    public Object remove(String name) {
        return getCurrentScopeContainer().remove(name);
    }

    @Override
    public void registerDestructionCallback(String name, Runnable callback) {
        getCurrentScopeContainer().registerDestructionCallback(name, callback);
    }

    @Override
    public Object resolveContextualObject(String name) {
        return null;
    }

    @Override
    public String getConversationId() {
        return getCurrentContext().toString();
    }

    private Context getCurrentContext(){
        return vertx.getOrCreateContext();
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
        configurableListableBeanFactory.registerScope(VERTICLE_INSTANCE_NAME,this);
    }

    private ScopedBeansContainer getCurrentScopeContainer() {
        Object o = getCurrentContext().get(VERTICLE_INSTANCE_SCOPE);
        if(o == null){
            o = new ScopedBeansContainer();
            getCurrentContext().put(VERTICLE_INSTANCE_SCOPE,o);
        }
        return (ScopedBeansContainer) o;
    }

    /**
     * Container for all beans used in the active scope.
     */
    private static class ScopedBeansContainer {

        private final Map<String, ScopedBeanReference> references = new ConcurrentHashMap<>();

        /**
         * Gets or creates the bean with the given name using the given object factory.
         *
         * @param name The name of the bean.
         * @param objectFactory The object factory used to create new instances.
         * @return The bean associated with the given name.
         */
        public Object getOrCreate(final String name, final ObjectFactory<?> objectFactory) {
            return this.references.computeIfAbsent(name, key -> new ScopedBeanReference(objectFactory))
                    .getBean();
        }

        /**
         * Removes the bean with the given name from this scope.
         *
         * @param name The name of the bean to remove.
         * @return The bean instances that was removed from the scope or null, if it wasn't present.
         */
        public Object remove(final String name) {
            final ScopedBeanReference ref = this.references.remove(name);
            if (ref == null) {
                return null;
            } else {
                return ref.getBeanIfExists();
            }
        }

        /**
         * Attaches a destruction callback to the bean with the given name.
         *
         * @param name The name of the bean to attach the destruction callback to.
         * @param callback The callback to register for the bean.
         */
        public void registerDestructionCallback(final String name, final Runnable callback) {
            final ScopedBeanReference ref = this.references.get(name);
            if (ref != null) {
                ref.setDestructionCallback(callback);
            }
        }

        /**
         * Destroys all beans in the scope and executes their destruction callbacks.
         */
        public void destroy() {
            final List<RuntimeException> errors = new ArrayList<>();
            final Iterator<ScopedBeanReference> it = this.references.values().iterator();
            while (it.hasNext()) {
                ScopedBeanReference val = it.next();
                it.remove();
                try {
                    val.destroy();
                } catch (RuntimeException e) {
                    errors.add(e);
                }
            }
            if (!errors.isEmpty()) {
                RuntimeException rex = errors.remove(0);
                for (RuntimeException error : errors) {
                    rex.addSuppressed(error);
                }
                throw rex;
            }
        }

    }

    /**
     * Container for a single scoped bean. This class manages the bean creation
     */
    private static class ScopedBeanReference {

        private final ObjectFactory<?> objectFactory;
        private Object bean;
        private Runnable destructionCallback;

        /**
         * Creates a new scoped bean reference using the given object factory.
         *
         * @param objectFactory The object factory used to create instances of that bean.
         */
        public ScopedBeanReference(ObjectFactory<?> objectFactory) {
            this.objectFactory = objectFactory;
        }

        /**
         * Gets or creates the bean managed by this instance.
         *
         * @return The existing or newly created bean instance.
         */
        public synchronized Object getBean() {
            if (this.bean == null) {
                this.bean = this.objectFactory.getObject();
            }
            return this.bean;
        }

        /**
         * Gets the bean managed by this instance, if it exists.
         *
         * @return The existing bean or null.
         */
        public Object getBeanIfExists() {
            return this.bean;
        }

        /**
         * Sets the given callback used to destroy the managed bean.
         *
         * @param destructionCallback The destruction callback to use.
         */
        public void setDestructionCallback(final Runnable destructionCallback) {
            this.destructionCallback = destructionCallback;
        }

        /**
         * Executes the destruction callback if set and clears the internal bean references.
         */
        public synchronized void destroy() {
            Runnable callback = this.destructionCallback;
            if (callback != null) {
                callback.run();
            }
            this.bean = null;
            this.destructionCallback = null;
        }

        @Override
        public String toString() {
            return "ScopedBeanReference [objectFactory=" + this.objectFactory + ", bean=" + this.bean + "]";
        }

    }
}
